﻿namespace CookieTerm
{
    using System;
    using System.Net.Sockets;
    using System.Threading;
    using System.Net;

    public class Terminal
    {
        public event EventHandler OnConnectionSucceeded;
        public event EventHandler<ConnectionFailedEventArgs> OnConnectionFailed;
        public event EventHandler OnDisconnected;
        public event EventHandler<DataAvailableEventArgs> OnDataAvailable;

        protected Thread connectingThread;
        protected string hostname;
        protected int port;
        protected Socket Socket { get; set; }

        public virtual string Hostname
        {
            get
            {
                return this.hostname;
            }
            set
            {
                ThrowIfConnected();
                this.hostname = value;
            }
        }

        public virtual int Port
        {
            get
            {
                return this.port;
            }
            set
            {
                ThrowIfConnected();

                if (value > 0)
                {
                    this.port = value;
                }
                else
                {
                    throw (new ArgumentException("TerminalClient: Port number must be positive."));
                }
            }
        }

        public virtual int TerminalWidth { get; set; }
        public virtual int TerminalHeight { get; set; }
        public virtual string TerminalType { get; set; }

        public virtual bool Connected
        {
            get
            {
                return (Socket != null && Socket.Connected);
            }
        }

        public virtual bool Connecting
        {
            get
            {
                return this.connectingThread != null;
            }
        }

        public virtual void BeginConnect()
        {
            if (this.connectingThread != null || Connected)
            {
                return;
            }

            this.connectingThread = new Thread(new ThreadStart(Connect));
            this.connectingThread.Start();
        }

        public virtual void Send(byte[] data)
        {
        }

        public virtual void Receive()
        {
        }

        public virtual void Disconnect()
        {
            if (this.connectingThread != null)
            {
                this.connectingThread.Abort();
            }

            if (this.Socket != null && this.Socket.Connected)
            {
                this.Socket.Shutdown(SocketShutdown.Both);
                this.Socket.Close();
            }

            InvokeOnDisconnected(new EventArgs());
        }

        protected void ThrowIfConnected()
        {
            if (this.Connected)
            {
                throw new InvalidOperationException("TerminalClient: Properties cannot be changed when connected.");
            }
        }

        protected virtual void Connect()
        {
            if (string.IsNullOrEmpty(Hostname))
            {
                InvokeOnConnectionFailed(new ConnectionFailedEventArgs("TerminalClient: Hostname must be set before connecting"));
            }

            if (Port == 0)
            {
                InvokeOnConnectionFailed(new ConnectionFailedEventArgs("TerminalClient: Port must be set before connecting"));
            }

            try
            {
                IPHostEntry ipHostEntry = Dns.GetHostEntry(Hostname);
                IPAddress ipAddress = ipHostEntry.AddressList[0];
                IPEndPoint remoteEP = new IPEndPoint(ipAddress, port);

                this.Socket = new Socket(AddressFamily.InterNetwork, SocketType.Stream, ProtocolType.Tcp);
                this.Socket.Connect(remoteEP);
                
                AfterConnected();

                InvokeOnConnectionSucceeded(new EventArgs());
            }
            catch (Exception ex)
            {
                if (!(ex is ThreadAbortException))
                {
                    InvokeOnConnectionFailed(new ConnectionFailedEventArgs(ex.Message));
                }
            }
            finally
            {
                this.connectingThread = null;
            }
        }

        protected virtual void AfterConnected()
        {
        }

        protected virtual void InvokeOnConnectionSucceeded(EventArgs args)
        {
            if (this.OnConnectionSucceeded != null)
            {
                this.OnConnectionSucceeded(this, args);
            }
        }

        protected virtual void InvokeOnConnectionFailed(ConnectionFailedEventArgs args)
        {
            if (this.OnConnectionFailed != null)
            {
                this.OnConnectionFailed(this, args);
            }
        }

        protected virtual void InvokeOnDisconnected(EventArgs args)
        {
            if (this.OnDisconnected != null)
            {
                this.OnDisconnected(this, args);
            }
        }

        protected virtual void InvokeOnDataAvailable(DataAvailableEventArgs args)
        {
            if (this.OnDataAvailable != null)
            {
                this.OnDataAvailable(this, args);
            }
        }

        #region Cleanup

        public void Close()
        {
            Dispose();
        }

        public void Dispose()
        {
            GC.SuppressFinalize(this);
            Dispose(true);
        }

        protected void Dispose(bool disposing)
        {
            if (disposing)
            {
                Disconnect();
            }
        }

        ~Terminal()
        {
            Dispose(false);
        }

        #endregion
    }
}
